Skip to content
将对象转换为 JSON 字符串
概述
JSON 字符串化是将内存中的数据结构转换为符合 JSON 规范的文本表示的过程。在 JavaScript 中,这一操作的标准实现是 JSON.stringify,但理解其内部机制需要从类型分发和递归遍历两个维度切入。
基本概念
JSON 规范支持的六种数据类型构成了序列化器需要处理的值域:
- null
- boolean
- number(整数与浮点数)
- string(双引号包裹的 Unicode 文本)
- array(有序值列表)
- object(无序的字符串键到 JSON 值的映射)
不在上述类型范围内的值(如 undefined、函数、Symbol)在完整实现中需要特殊处置,但在受限类型系统中可以从简处理。
实现
JSON.stringify 的核心逻辑是一个按类型分发的递归遍历器。入口函数读取当前值的类型标签,根据标签决定当前层的输出格式。当遇到容器类型(数组或对象)时,递归进入子元素,重复同一套类型判断流程。递归的终止条件是当前值为基本类型——此时直接产生对应的字符串字面量,不再产生新的递归调用。
js
function jsonStringify(value) {
if (value === null) return 'null'
if (typeof value === 'number' || typeof value === 'boolean')
return String(value)
if (typeof value === 'string') return `"${value}"`
if (Array.isArray(value)) {
const items = value.map((v) => jsonStringify(v))
return `[${items.join(',')}]`
}
if (typeof value === 'object') {
const pairs = Object.entries(value).map(
([key, val]) => `"${key}":${jsonStringify(val)}`
)
return `{${pairs.join(',')}}`
}
return ''
}递归深度由输入对象的最大嵌套层级决定。每一层递归在调用栈上占据一帧,因此极端深层嵌套的对象可能导致栈溢出——但在题目限定的输入范围内,这不是主要关注点。
示例
基本类型
js
jsonStringify(null) // 'null'
jsonStringify(42) // '42'
jsonStringify(-3.14) // '-3.14'
jsonStringify(true) // 'true'
jsonStringify('hello') // '"hello"'null 通过首行提前返回。数字和布尔值经 String() 转换后直接输出为对应字面量,不附加引号。字符串本身需要输出的引号通过模板字面量拼接完成。
数组
js
jsonStringify([1, 'a', null, true])
// '[1,"a",null,true]'数组中每个元素独立调用 jsonStringify,各自的返回字符串由逗号连接后包裹在方括号内。嵌套数组同理,递归进入下一层时走同一套数组分支:
js
jsonStringify([[1, 2], [3, 4]])
// '[[1,2],[3,4]]'对象
js
jsonStringify({ name: 'Alice', age: 30 })
// '{"name":"Alice","age":30}'Object.entries 遍历对象自身的可枚举属性,每对键值被格式化为 "key":递归结果 的形式,通过逗号拼接后包裹花括号。键名始终输出为双引号字符串。嵌套对象在递归过程中自然处理:
js
jsonStringify({ user: { id: 1, active: false } })
// '{"user":{"id":1,"active":false}}'混合嵌套
js
jsonStringify({
title: 'post',
tags: ['js', 'json'],
meta: { version: 1 }
})
// '{"title":"post","tags":["js","json"],"meta":{"version":1}}'每个分支独立决策,互不干扰。数组分支和对象分支交替出现在调用栈中,不存在谁先谁后的固定顺序。
注意点
字符串转义
上述实现未处理字符串中的特殊字符。如果字符串值本身包含双引号、反斜杠或控制字符,输出的 JSON 文本将不符合规范且无法被合法解析。以双引号为例:
js
// 当前实现的行为
jsonStringify('say "hello"')
// '"say "hello""'
// 这是非法 JSON——解析器将报语法错误正确处理方式是遍历字符串内容,对需要转义的字符替换为对应的转义序列:
| 字符 | 转义序列 |
|---|---|
" | \" |
\ | \\ |
/ | \/(可选) |
\n | \\n |
\r | \\r |
\t | \\t |
\b | \\b |
\f | \\f |
| 其他控制字符 | \\uXXXX |
符合规范的实现需要维护一个转义映射表,在字符串分支内做替换遍历。
科学记数法与特殊数字值
数字类型在规范中存在三个特殊值:NaN、Infinity、-Infinity。它们在 JSON 规范中不是合法的数字表示,JSON.stringify 会将这三个值统一序列化为 "null"。上述实现中,String(NaN) 直接输出 "NaN",与规范行为不一致。
此外,极大的整数和极小的浮点数在 String() 转换时可能产生科学记数法表示(如 1e100),JSON 解析器接受这种格式,但具体行为取决于 Number.prototype.toString 的实现。
undefined 与函数
undefined 和函数值不在 JSON 值域内。JSON.stringify 在遇到这两种值时的行为取决于它们出现的位置:
- 作为对象属性值:该属性被跳过,不出现在结果中。
- 作为数组元素:输出
null占位。
当前实现不处理这两种情况——jsonStringify(undefined) 会走到 typeof value === 'object' 的 false 分支并返回空字符串,而函数会触发 Object.entries 产生 TypeError。
循环引用
JSON.stringify 在检测到循环引用时抛出 TypeError: Converting circular structure to JSON。上述递归实现没有检测循环引用的机制——如果输入对象包含自引用,它将无限递归直到栈溢出。实际场景中,需要维护一个已访问对象的集合(WeakSet),在每次进入对象分支前检查当前对象是否已在其中。
toJSON 方法
如果被序列化的对象定义了 toJSON 方法,JSON.stringify 会调用该方法并将其返回值作为实际序列化目标,而非直接处理原对象。题目约束的类型定义暂不涵盖此场景。
限制
题目定义的 JSONValue 类型将输入限制在一个安全子集内——不含 undefined、函数、循环引用、以及需要转义的特殊字符。这意味着一份解决此题的简洁实现无法直接替代 JSON.stringify,但它清晰地展示了序列化器的核心分发骨架:类型判断 → 递归进入 → 拼接输出。
在完整的序列化实现中,上述四个未处理的情况(转义、特殊值、undefined/函数、循环引用)各自需要一个独立分支。将它们嵌入现有骨架后,整体结构会膨胀数倍,但基本的分发逻辑保持不变。
